

package redeS.mensagem;

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.ListIterator;
import peersim.cdsim.CDProtocol;
import peersim.cdsim.CDState;
import peersim.config.Configuration;
import peersim.core.CommonState;
import peersim.core.Network;
import peersim.core.Node;
import redeS.arquitetura.*;
import redeS.mensagem.requisicao.*;
import redeS.p2pwsrep.QueryReputation;
import redeS.p2pwsrep.ReputationControl;
import redeS.visualizacao.P2PObserver;
import redeS.visualizacao.P2PObserverConstants;

/**
 * Protocolo das mensagens Gnutella
 * Os nós podem operar com mensagens desde que eles tenham esse protocolo.
 * @author thiago-pc
 *
 */
public class GnuMessageProtocol implements CDProtocol, peersim.core.Cleanable {

	private String prefix;
	private int pid = -1;
		
	/**
	 * Probabilidade desse nó ter um firewall (0 <= prob <= 100)
	 */
	public static String PAR_FIRWPROB = "firewall_prob";
	private boolean firewall;
	
	/**
	 * Número de arquivos que esse nó terá.
	 */
	public static String PAR_NUMFILES = "number_of_files";
	private int numberOfFiles;
	
	/**
	 * Variação do número de arquivos em relação ao parâmetro {@link #PAR_NUMFILES}.
	 */
	public static String PAR_NFILE_MARG = "number_of_files_variation";
	private int nFilesVariation;
	
	// fila de mensagens Gnutella que esse nó tem e falta repassar
	private LinkedList<GnutellaMessage> queueMessage;
	// fila de pais da mensagens Gnutella acima: o índice das duas filas são sincronizadas
	// (a i-ézima mensagem em queueMessage tem o i-ézimo pai em queueMessageFathers
	private LinkedList<Integer> queueMessageFathers;
	
	private LinkedList<GnutellaMessage> auxQueueMessage;
	private LinkedList<Integer> auxQueueMessageFathers;
	
	// lista de novas mensagens que esse nó gerou nesse ciclo, digo nesse ciclo porque
	// estou supondo que você executará o método getNewMensagens a cada ciclo na classe
	// de controle, que é o certo!
	private ArrayList<GnutellaMessage> newMessages;
	
	// lista de requisições que esse nó fez e está aguardando uma resposta
	private ArrayList<Request> requests;
	
	public GnuMessageProtocol(String prefix){
		this.prefix = prefix;
		if(Configuration.contains(prefix + ".protocol") ){
			pid = Configuration.getPid(prefix + ".protocol");
		}
		if(pid<0){
			System.err.println("Especifique a string identificadora do protocolo Gnutella");
			System.exit(1);
		}
		
		queueMessage = new LinkedList<GnutellaMessage>();
		queueMessageFathers = new LinkedList<Integer>();
		auxQueueMessage = new LinkedList<GnutellaMessage>();
		auxQueueMessageFathers = new LinkedList<Integer>();
		
		firewall = (CommonState.r.nextInt()%100) < 
					Configuration.getInt(prefix + "." + PAR_FIRWPROB, 0);
		
		numberOfFiles = Configuration.getInt(prefix + "." + PAR_NUMFILES, 0);
		nFilesVariation = Configuration.getInt(prefix + "." + PAR_NFILE_MARG, 0);
		numberOfFiles += nFilesVariation>0? CommonState.r.nextInt() % nFilesVariation : 0;
		if(numberOfFiles<0) numberOfFiles *= -1;
		GnuMessageControl.numberOfFiles += numberOfFiles;
		
		newMessages = new ArrayList<GnutellaMessage>();
		requests = new ArrayList<Request>();
	}
	

	/**
	 * Retorna as mensagens geradas por esse nó (protocolo). Ao chamar esse método
	 * a lista de novas mensagens esvazia.
	 */
	public ArrayList<GnutellaMessage> getNewMessages(){
		ArrayList<GnutellaMessage> resp = new ArrayList<GnutellaMessage>();
		
		while(!newMessages.isEmpty()){
			resp.add(newMessages.get(0));
			newMessages.remove(0);
		}
		return resp;
	}
	
	/**
	 * Esse nó tem firewall?
	 * @return boolean firewall
	 */
	public boolean containsFirewall(){
		return firewall;
	}
	
	/**
	 * Número de arquivos desse nó.
	 * @return int numberOfFiles
	 */
	public int getNumberOfFiles(){
		return numberOfFiles;
	}
	
	/**
	 * O método clone desse protocolo está sobrecarregado e gera uma nova classe.
	 * @return new GnuMessageProtocol(prefix)
	 */
	@Override
	public Object clone(){
		return new GnuMessageProtocol(prefix);
	}
	
	/**
	 * Insere uma requisição para esse nó baseado numa mensagem <b>de requisição</b>
	 * da rede Gnutella passado como parâmetro. Também é necessário informar qual o
	 * índice desse nó que quer criar a requisição <s>pois infelizmente os protocolos,
	 * nesse simulador, desconhecem o nó a qual se encontram</s>.</br>
	 * <b>Obs.: As mensagens passadas como parâmetro pela classe de controle são recebidas
	 * por esse nó automaticamente.</b>
	 * @param message Especifica a requisição. (ex.: Ping, Query, QueryHit firewalled)
	 * @param indexThisNode Índice desse nó na rede.
	 */
	public void addRequest(GnutellaMessage message, int indexThisNode){
		
		// PingRequest - vem da classe de controle
		if(message instanceof Ping){
			// coloca o PingRequest na lista. a classe de cotrole já deve ter colocado
			// esse Ping em sua lista de mensagens atuais na rede
			requests.add(new PingRequest((Ping) message));
			// coloca esse Ping nesse nó para ser repassado
			sendExcept(message, indexThisNode, indexThisNode);
		}
		// QueryRequest - vem da classe de controle
		else if(message instanceof Query){
			// coloca o QueryRequest na lista. a classe de controle já deve ter colocado
			// esse Query em sua lista de mensagens atuais na rede
			requests.add(new QueryRequest((Query) message));
			// coloca esse Query nesse nó para ser repassado
			sendExcept(message, indexThisNode, indexThisNode);
		}
		// PushRequest - vem dessa própria classe ao gerar um QueryHit tendo, este nó,
		// um firewall. Essa requisição é diferente pois não gera uma mensagem para
		// ser repassada
		else if(message instanceof QueryHit){
			// coloca o PushRequest na lista. a classe de controle vai colocar esse
			// QueryHit em sua lista de mensagens atuais assim que chamar o método
			// getNewMessages
			requests.add(new PushRequest((QueryHit) message));
		}
		else if(message instanceof QueryReputation){
			// apenas repassa o QuerReputation, pois essa mensagem só serve para
			// identificar os nós alcançáveis
			sendExcept(message, indexThisNode, indexThisNode);
		}
	}
	
	@Override
	public void nextCycle(Node node, int protocolID) {
		GnutellaMessage currentMessage;
		int currentFather;
		
		while(!auxQueueMessage.isEmpty()){
			
			// /*******************/ Classe observadora /*******************/
			P2PObserver.setData(P2PObserverConstants.MAXMESSAGES,
					(float)auxQueueMessage.size(), " ciclo num. " + CDState.getCycle());
			// /************************************************************/
			
			// Pega uma mensagem da pilha
			currentMessage = auxQueueMessage.removeFirst();
			currentFather = auxQueueMessageFathers.removeFirst();
			
			//Decrementa TTL
			if(currentMessage.getTTL() < 0) return;
			currentMessage.TTLDecrement();
			
			
			// Trata ela conforme seu tipo
			
			// se for Ping, gera o Pong, envia o Pong, e coloca o Pong em newMessages
			// sempre repassa o Ping
			if(currentMessage instanceof Ping){
				Pong newPong = new Pong(currentMessage.getDescriptorID(),
						currentMessage.getHops(),node.getIndex(),numberOfFiles);
				newMessages.add(newPong);
				sendOnly(newPong, currentFather, node.getIndex());
				sendExcept(currentMessage, currentFather, node.getIndex());
				
				// /*******************/ Classe observadora /*******************/
				P2PObserver.setData(P2PObserverConstants.TOTALPING, (float)1, null);
				// /************************************************************/
			}
			// se for Pong, verifica se é uma resposta a um Ping que este nó requisitou
			// se for, a mensagem morre aki, senão, repassa o Pong
			else if(currentMessage instanceof Pong){
				Request request = requestsGetAnswer(currentMessage);
				if(request != null){
					/** ATENÇÃO!!! TRATAR O RECEBIMENTO DE UM PONG REQUISITADO AQUI! */
				}
				else{
					sendExcept(currentMessage,currentFather,node.getIndex());		
				}
				// /*******************/ Classe observadora /*******************/
				P2PObserver.setData(P2PObserverConstants.TOTALPONG, (float)1, null);
				// /************************************************************/
			}
			// se for um Query, gera um QueryHit conforme sua probabilidade de ter o
			// recurso, coloca o QueryHit em newMessages e o envia. nesse caso, se esse
			// nó tiver firewall o QueryHit gerado será firewalled e este nó ficará
			// aguardando um Push.
			// independente se tiver ou não o recurso sempre repassa o query.
			else if(currentMessage instanceof Query){
				
				double prob = ((double)numberOfFiles) / GnuMessageControl.numberOfFiles;
				if(Math.random() < prob + ((Query)currentMessage).getPopularity()){
					
					QueryHit newQHit = new QueryHit(currentMessage.getDescriptorID(),
							currentMessage.getHops(),node.getIndex(),firewall);
					if(firewall){
						addRequest(newQHit,node.getIndex());
					}
					newMessages.add(newQHit);
					sendOnly(newQHit, currentFather,node.getIndex());
				}
				sendExcept(currentMessage, currentFather,node.getIndex());
				
				// /*******************/ Classe observadora /*******************/
				P2PObserver.setData(P2PObserverConstants.TOTALQUERY, (float)1, null);
				// /************************************************************/
			}
			// se for QueryHit verifica se é uma resposta a um Query que este nó enviou
			// se for a mensagem morre aki e lança um Push se tiver firewall, se não for,
			// repassa a mensagem
			else if(currentMessage instanceof QueryHit){
				Request request = requestsGetAnswer(currentMessage);
				if(request != null){
					/** ATENÇÃO!!! TRATAR O RECEBIMENTO DE UM QUERYHIT REQUISITADO AQUI! */
								
					// abaixo, envia o Push se o QueryHit tiver firewall
					if( ((QueryHit)currentMessage).isFirewalled() ){
						Push newPush = new Push(currentMessage.getDescriptorID(),
								currentMessage.getHops(),
								((QueryHit)currentMessage).getIndexOfNode());
						newMessages.add(newPush);
						sendOnly(newPush, currentFather,node.getIndex());
					}
				}
				else{
					sendExcept(currentMessage, currentFather,node.getIndex());
				}
				
				// /*******************/ Classe observadora /*******************/
				P2PObserver.setData(P2PObserverConstants.TOTALQUERYHIT, (float)1, null);
				// /************************************************************/
			}
			// se for um Push verifica se é uma resposta a um QueryHit que esse nó enviou
			// sendo que este nó é firewalled, se for o Push morre aki, se não for, repassa
			else if(currentMessage instanceof Push){
				Request request = requestsGetAnswer(currentMessage);
				if(request != null){
					/** ATENÇÃO!!! TRATAR O RECEBIMENTO DE UM PUSH REQUISITADO AQUI! */
				}
				else{
					sendExcept(currentMessage, currentFather,node.getIndex());
				}
				// /*******************/ Classe observadora /*******************/
				P2PObserver.setData(P2PObserverConstants.TOTALPUSH, (float)1, null);
				// /************************************************************/
			}
			else if(currentMessage instanceof QueryReputation){
				((QueryReputation)currentMessage).getFather().addDiscoveredNode(node);
				sendExcept(currentMessage, currentFather, node.getIndex());
			}
			/** ELSE-IF DE MAIS MENSAGENS AKI !!!! */
			
			// retira as requisições expiradas
			Request reqCur;
			for(int i=0; i<requests.size(); i++){
				reqCur = requests.get(i);
				if(reqCur.getTTL() > 0){
					reqCur.TTLDecrement();
				}
				else{
					// /*******************/ Classe observadora /*******************/
					if(reqCur instanceof PingRequest && reqCur.getNumberOfAnswers() > 0){
						/*P2PObserver.setData(P2PObserverConstants.DISCOVERED,
								(float)reqCur.getNumberOfAnswers(), null);						
						P2PObserver.setData(P2PObserverConstants.DISCOVEREDFILES,
								(float)((PingRequest)reqCur).getNumberOfFiles(),null);*/
					}
					else if(reqCur instanceof QueryRequest){
						if(reqCur.getNumberOfAnswers()>0){
							P2PObserver.setData(P2PObserverConstants.SEARCHFOUND,
									(float)1, null);
							P2PObserver.setData(P2PObserverConstants.FIREWALLFOUND,
									(float)((QueryRequest)reqCur).getFirewalledAnswers(),null);
						}
						else{
							P2PObserver.setData(P2PObserverConstants.SEARCHNOTFOUND,
									(float)1, null);
						}
					}
					// /************************************************************/
					requests.remove(i);
					i--;
				}
			}
		}
		
		while(!auxQueueMessage.isEmpty()){
			auxQueueMessage.removeFirst();
			auxQueueMessageFathers.removeFirst();
		}
		
		while(!queueMessage.isEmpty()){
			auxQueueMessage.addFirst(queueMessage.removeLast());
			auxQueueMessageFathers.addFirst(queueMessageFathers.removeLast());
		}
		
	}
	
	@Override
	public void onKill() {
		GnuMessageControl.numberOfFiles -= numberOfFiles;
	}	
		
	//------------------------------------------------------------//
	/******************* METODOS PRIVADOS ********************//***/
	//------------------------------------------------------------//
	
	// envia a mensagem somente para o pai dela
	private void sendOnly(GnutellaMessage message, int father, int thisNode){
		GnuMessageProtocol fatherProtocol = null;
		Node fatherNode = Network.get(father);

		// se foi removido pela outra classe de controle, nada faz
		if(fatherNode == null){
			return;
		}
		
		for(int i=0; i<fatherNode.protocolSize(); i++){
			if(fatherNode.getProtocol(i) instanceof GnuMessageProtocol){
				fatherProtocol = (GnuMessageProtocol)fatherNode.getProtocol(i);
				break;
			}
		}
		fatherProtocol.receiveMessage(message, thisNode, fatherNode.getIndex());
	}
	
	// envia a mensagem para todos menos para o pai dela
	private void sendExcept(GnutellaMessage message, int father, int thisNode){
		GnuMessageProtocol currentProtocol;
		Node currentNode;
		GnutellaProtocol thisProtocol;
		
		try{
		thisProtocol = (GnutellaProtocol)Network.get(thisNode).getProtocol(pid);
		}
		catch(NullPointerException removedNodeExeption){
			// se foi removido pela outra classe de controle, nada faz
			return;
		}
		
		for(int i=0; i<thisProtocol.degree(); i++){
			currentNode = thisProtocol.getNeighbor(i);
			if(currentNode.getIndex()==father){
				continue;
			}
			currentProtocol = null;
			for(int j=0; j<currentNode.protocolSize(); j++){
				if(currentNode.getProtocol(j) instanceof GnuMessageProtocol){
					currentProtocol = (GnuMessageProtocol)currentNode.getProtocol(j);
					break;
				}
			}
			currentProtocol.receiveMessage(message, thisNode, currentNode.getIndex());
		}
	}
	
	/**
	 * Método que permite esse nó receber mensagens de outros nós.
	 * @param message GnutellaMessage a ser recebida
	 * @param father quem está enviando essa mensagem
	 */
	// Esse método é privado para que somente outros protocolos (de outros nós)
	// iguais a esse possam acessá-lo.
	private void receiveMessage(GnutellaMessage message, int father, int thisNode){
		if(message.getTTL() > 0){
			if(!queueMessage.contains(message) && !auxQueueMessage.contains(message)){
				queueMessage.addLast(message);
				queueMessageFathers.addLast(father);
			}
		}
	}
	
	// verifica se a mensagem é uma resposta a uma requisição desse nó
	// se for, retorna a requisição, caso contrário, null
	private Request requestsGetAnswer(GnutellaMessage message){
		ListIterator<Request> iterator = requests.listIterator();
		Request reqCur;
		while(iterator.hasNext()){
			reqCur = iterator.next();
			if(reqCur.isAnswer(message)){
				return reqCur;
			}
		}
		return null;
	}
}
